﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Reflection;
using System.Linq.Expressions;
using System.Collections;

using King.Loader;
using King.CSharp.Reflection;
using King.Text;
using King.Extensions;
using King.Reflection;
using King.Text.Html;
using King.Declarative;
using System.Text.RegularExpressions;
using King.Rest.Reflection;

namespace King.Http {

    internal sealed class HttpPageNotFoundException : Exception {
        public HttpPageNotFoundException(HttpListenerRequest request) {
        }
    }

    public sealed class HttpPageAttribute : Attribute {
    }

    public sealed class HttpServer {

        private readonly HttpListener m_listener;
        private readonly RestXmlLoader m_restLoader;
        private readonly CSharpLoader m_csharpLoader;
        private readonly Loader<CSharpAssembly, HttpAssembly> m_assemblies;

        private readonly object m_context;
        private readonly Lazy<HttpPageInfo> m_defaultPage;
        private readonly HttpUrl m_defaultUrl;
        private readonly string m_host;
        private readonly int? m_port;

        public HttpServer(
            string host,
            int? port,
            Type defaultPage = null,
            object context = null
        ) {
            m_host = host;

            m_csharpLoader = CSharpLoader.Create();
            m_defaultPage = new Lazy<HttpPageInfo>(() => GetPage(defaultPage));
            m_context = context;

            m_defaultUrl = new HttpUrl(host);
            m_listener = new HttpListener();
            m_listener.Prefixes.Add(m_defaultUrl.ToString());

            m_assemblies = new Loader<CSharpAssembly, HttpAssembly>(
                keys: o => Enumerable.Empty<object>(),
                defaultResolver: o => {
                    return new HttpAssembly(this, o.Key);
                },
                getLoader: o => o.Server.m_assemblies,
                getKeys: o => new object[] { 
                    o.Name.ToLower(),
                    o.Assembly
                },
                normalizeKey: o => {
                    var name = o;

                    // assembly
                    var assembly = o as CSharpAssembly;

                    // string
                    if (assembly == null)
                        assembly = m_csharpLoader.TryGetAssembly(o as string);

                    // AssemblyName
                    if (assembly == null)
                        assembly = m_csharpLoader.TryGetAssembly(o as AssemblyName);

                    // CSharpAssemblyName
                    if (assembly == null)
                        assembly = m_csharpLoader.TryGetAssembly(o as CSharpAssemblyName);

                    return assembly;
                }
            );
        }

        private string Resolve(HttpListenerRequest request) {
            var page = TryGetPage(request.Url);
            if (page == null)
                return string.Empty;

            var textReader = page.Evaluate(request);
            var text = textReader.ReadToEnd();

            return text;
        }

        public string Host {
            get { return m_host; }
        }
        public int? Port {
            get { return m_port; }
        }
        public HttpUrl DefaultUrl {
            get { return m_defaultUrl; }
        }
        public HttpPageInfo DefaultPage {
            get { return m_defaultPage.Value; }
        }

        public object Context {
            get { return m_context; }
        }
        public async Task StartAsync() {
            m_listener.Start();

            while (true) {

                // get request
                var context = await m_listener.GetContextAsync();
                var request = context.Request;

                // set response
                var htmlResponse = Resolve(request);
                var response = context.Response;
                response.ContentLength64 = htmlResponse.Length;

                // encode response
                var encodedResponse = Encoding.UTF8.GetBytes(htmlResponse);
                using (var stream = response.OutputStream)
                    stream.Write(encodedResponse, 0, encodedResponse.Length);
            }
        }

        public HttpAssembly TryGetAssembly(string name) {
            return m_assemblies.TryGet(name);
        }
        public HttpAssembly GetAssembly(string name) {
            return m_assemblies.Get(name);
        }
        public HttpAssembly TryGetAssembly(Assembly assembly) {
            return TryGetAssembly(m_csharpLoader.TryGetAssembly(assembly));
        }
        public HttpAssembly GetAssembly(Assembly assembly) {
            return GetAssembly(m_csharpLoader.GetAssembly(assembly));
        }
        public HttpAssembly TryGetAssembly(CSharpAssembly assembly) {
            return m_assemblies.TryGet(assembly);
        }
        public HttpAssembly GetAssembly(CSharpAssembly assembly) {
            return m_assemblies.Get(assembly);
        }
        public IEnumerable<HttpAssembly> Assemblies() {
            return m_assemblies.Values();
        }

        public HttpPageInfo TryGetPage(HttpUrl url) {
            if (url == null)
                return null;

            // default page
            if (url.Equals(DefaultUrl))
                return DefaultPage;

            // hosts must agree
            if (url.Host.ToLower() != m_host.ToLower())
                return null;

            // resolve assembly
            var assembly = TryGetAssembly(url.AssemblyName);
            if (assembly == null)
                return null;

            // resolve page
            var page = assembly.TryGetPage(url.PageName);
            if (page == null)
                return null;

            return page;
        }
        public HttpPageInfo GetPage(HttpUrl url) {
            var page = TryGetPage(url);
            if (page == null)
                throw new ArgumentException();
            return page;
        }
        public HttpPageInfo TryGetPage(Uri uri) {
            return TryGetPage(HttpUrl.TryParse(uri));
        }
        public HttpPageInfo GetPage(Uri uri) {
            var page = TryGetPage(uri);
            if (page == null)
                throw new ArgumentException();
            return page;
        }
        public HttpPageInfo TryGetPage(string url) {
            return TryGetPage(HttpUrl.TryParse(url));
        }
        public HttpPageInfo GetPage(string url) {
            var page = TryGetPage(url);
            if (page == null)
                throw new ArgumentException();
            return page;
        }

        public HttpPageInfo TryGetPage(Type type) {
            if (type == null)
                return null;

            var assembly = TryGetAssembly(type.Assembly);
            if (assembly == null)
                return null;

            return assembly.TryGetPage(type.FullName);
        }
        public HttpPageInfo GetPage(Type type) {
            if (type == null)
                throw new ArgumentNullException();

            return GetAssembly(type.Assembly).GetPage(type.FullName);
        }
    }
    public sealed class HttpAssembly {
        private readonly HttpServer m_server;
        private readonly CSharpAssembly m_assembly;
        private readonly Loader<CSharpType, HttpPageInfo> m_pages;

        internal HttpAssembly(HttpServer server, CSharpAssembly assembly) {
            m_server = server;
            m_assembly = assembly;

            m_pages = new Loader<CSharpType, HttpPageInfo>(
                keys: o =>
                    from x in assembly.Types()
                    where x.IsDefined<HttpPageAttribute>() && !x.IsGenericType
                    select x,
                defaultResolver: o => new HttpPageInfo(this, o.Key),
                getLoader: o => o.Assembly.m_pages,
                getKeys: o => new object[] {
                    o.Type,
                    o.FullName
                },
                normalizeKey: o => {
                    var name = o as CSharpType;
                    if (name == null) {

                        var fullName = o as string;
                        if (fullName == null)
                            return null;

                        name = m_assembly.TryGetType(fullName);
                    }

                    if (name.Assembly != m_assembly)
                        return null;

                    return name;
                }
            );
        }

        public CSharpAssembly Assembly {
            get { return m_assembly; }
        }
        public HttpServer Server {
            get { return m_server; }
        }
        public string Name {
            get { return m_assembly.Name; }
        }

        public HttpPageInfo TryGetPage(string name) {
            return m_pages.TryGet(name);
        }
        public HttpPageInfo GetPage(string name) {
            return m_pages.Get(name);
        }
        public IEnumerable<HttpPageInfo> Pages() {
            return m_pages.Values();
        }

        public override string ToString() {
            return Name;
        }
    }
    public sealed class HttpPageInfo {
        private readonly HttpAssembly m_assembly;
        private readonly CSharpType m_type;

        internal HttpPageInfo(HttpAssembly assembly, CSharpType type) {
            m_type = type;
            m_assembly = assembly;
        }

        internal TextReader Evaluate(HttpListenerRequest request) {
            var url = request.Url;
            var queryString = url.Query;

            var regex = @"[?](?<name>\w*)=(?<value>\w*)";
            var match = Regex.Match(queryString, regex);
            var args1 = match.Groups["name"].Captures.Cast<Capture>();

            var args = match.Groups["name"].Captures.Cast<Capture>().ToArray()
                .Zip(match.Groups["value"].Captures.Cast<Capture>().ToArray(),
                    (n, v) => new KeyValuePair<string, string>(n.Value, v.Value)
                );

            return new HttpPage(this, request) {
                m_type.Load().Activate(args.Select(o => o.Value))
            }.ToTextReader();
        }

        public string Name {
            get { return m_type.Name; }
        }
        public string FullName {
            get { return m_type.MetadataFullName; }
        }
        public CSharpType Type {
            get { return m_type; }
        }
        public HttpAssembly Assembly {
            get { return m_assembly; }
        }
        public HttpServer Server {
            get { return Assembly.Server; }
        }
        public HttpUrl ToUrl(params object[] arguments) {
            return ToUrl((IEnumerable<object>)arguments);
        }
        public HttpUrl ToUrl(IEnumerable<object> arguments) {
            if (arguments == null)
                arguments = Enumerable.Empty<object>();

            var serializedArguments = Enumerable.Empty<KeyValuePair<string, string>>();

            return new HttpUrl(
                host: Server.Host,
                port: Server.Port,
                assemblyName: Assembly.Name,
                pageName: FullName,
                arguments: serializedArguments
            );
        }

        public override string ToString() {
            return Assembly.Name + ":" + FullName;
        }
    }
    public sealed class HttpUrl {
        internal static HttpUrl TryParse(string url) {
            if (url == null)
                return null;

            Uri uri;
            if (!Uri.TryCreate(url, UriKind.Absolute, out uri))
                return null;

            return TryParse(uri);
        }
        internal static HttpUrl TryParse(Uri uri) {
            var segments = uri.Segments.ToArray();
            if (segments.Length != 3)
                throw new ArgumentException();

            var assemblyName = segments[1].Trim('/');
            var pageName = segments[2].Trim('/');

            var queryString = uri.Query;

            var regex = @"[?](?<name>\w*)=(?<value>\w*)";
            var match = Regex.Match(queryString, regex);
            if (!match.Success)
                return null;

            var names = match.Captures("name");
            var values = match.Captures("value");
            if (names.Count() != values.Count())
                return null;

            var arguments = names.Zip(values,
                (name, value) => 
                    new KeyValuePair<string, string>(name, value)
            );

            return new HttpUrl(
                host: uri.Host,
                port: uri.Port,
                assemblyName: segments[1].Trim('/'),
                pageName: segments[2].Trim('/'), 
                arguments: arguments
            );
        }

        private readonly Lazy<Uri> m_uri;
        private readonly string m_host;
        private readonly string m_assemblyName;
        private readonly int? m_port;
        private readonly string m_pageName;
        private readonly IEnumerable<KeyValuePair<string, string>> m_arguments;

        internal HttpUrl(
            string host,
            int? port = null,
            string assemblyName = null,
            string pageName = null,
            IEnumerable<KeyValuePair<string, string>> arguments = null) {

            m_host = host;
            m_port = port;
            m_assemblyName = assemblyName;
            m_pageName = pageName;
            m_arguments = arguments ?? 
                Enumerable.Empty<KeyValuePair<string, string>>();

            m_uri = new Lazy<Uri>(() => 
                new Uri(
                    new TextJoin {
                        new TextJoin("/", prefix: "http://", suffix: "/") {
                            new TextJoin(":") { m_host, m_port },
                            m_assemblyName,
                            m_pageName,
                        },

                        new TextJoin("&", "?") {
                            m_arguments.Select(o =>
                                new TextJoin("=") { 
                                    o.Key,
                                    o.Value
                                }
                            )
                        }
                    }.ReadToEnd()
                )
            );
        }

        public string Host {
            get { return m_host; }
        }
        public string AssemblyName {
            get { return m_assemblyName; }
        }
        public string PageName {
            get { return m_pageName; }
        }
        public IEnumerable<KeyValuePair<string, string>> Arguments() {
            return m_arguments;
        }
        public Uri Uri {
            get { return m_uri.Value; }
        }

        public override string ToString() {
            return m_uri.Value.ToString();
        }
        public override bool Equals(object obj) {
            var lhs = obj as HttpUrl;
            if (lhs == null)
                return false;

            return lhs.Uri.Equals(Uri);
        }
        public override int GetHashCode() {
            return Uri.GetHashCode();
        }
    }

    public sealed class HttpPage : TextDeclaration {

        private HttpListenerRequest m_request;
        private HttpPageInfo m_info;

        internal HttpPage(HttpPageInfo info, HttpListenerRequest request) {
            m_request = request;
            m_info = info;
        }

        public HttpListenerRequest Request {
            get { return m_request; }
        }
        public HttpPageInfo Info {
            get { return m_info; }
        }

        protected override IEnumerable Substance() {
            return Substantial;
        }
    }
    public sealed class HttpLink : TextDeclaration {
        private readonly Expression<Func<object>> m_target;

        public HttpLink(Expression<Func<object>> target) {
            m_target = target;
        }

        private object Evaluate(params object[] values) {
            return values;
        }

        protected override IEnumerable Children(DeclarationContext frame) {
            var body = m_target.Body as NewExpression;
            if (body == null)
                throw new ArgumentException();

            // resolve source page
            var sourcePage = frame.Ancestors<HttpPage>().First();
            var server = sourcePage.Info.Server;
            var targetPage = server.TryGetPage(body.Constructor.DeclaringType);
            if (targetPage == null)
                throw new ArgumentException();

            // evaluate arguments
            var argumentExpressions = body.Arguments;
            var arguments = argumentExpressions.Select(o => {
                var expression = Expression.Lambda<Func<object>>(o, m_target.Parameters);
                var lambda = expression.Compile();
                var value = lambda();
                return value;
            }).ToArray();

            // serialize arguments into url
            var targetUrl = targetPage.ToUrl(arguments);

            // anchor tag
            return new HtmlAnchor(targetUrl) {
                base.Children()
            };
        }
        protected override IEnumerable Substance() {
            return Substantial;
        }
    }
}
